package kernel;

import java.util.LinkedList;
import java.util.PriorityQueue;

import model.Cliente;
import model.Evento;
import model.Fila;
import model.Servidor;

public class GeraEventoSim extends Valores {

	public double tempoUltimosIncrementosFila2;

	private static double tempoSimulacao;
	private LinkedList<RodadaSim> rodadas;

	private PriorityQueue<Evento> eventos;
	private GeraTempoExp taxaChegada;
	private GeraTempoExp taxaSaida;
	private Fila fila1Sistema;
	private Fila fila2Sistema;
	private Servidor servidorSistema;
	private String result = "";

	public static GeraEventoSim geradorEvSimulacao = null;

	private int idEntradaFreguesSistema;

	public static GeraEventoSim getInstance() {
		if (geradorEvSimulacao == null)
			geradorEvSimulacao = new GeraEventoSim();
		return geradorEvSimulacao;
	}

	//Construtor da classe cria as filas, geradores, e servidor
	public GeraEventoSim() {

		setTempoSimulacao(0);

		taxaChegada = new GeraTempoExp(Valores.txChegada, SEMENTE);
		taxaSaida = new GeraTempoExp(Valores.txServico, SEMENTE);

		eventos = new PriorityQueue<Evento>(1000000);
	
		setFila1Sistema(new Fila());
		setFila2Sistema(new Fila());

		setServidorSistema(new Servidor());

		setIdEntradaFreguesSistema(0);

		setRodadas(new LinkedList<RodadaSim>());
		setRodadaAtual(new RodadaSim(1));

		geraEventoChegadaFregues();
	}

	public boolean atenderEventoSeguinte() {

		Evento ev = null;
		//Confere se a fila de eventos foi devidamente criada
		try {
			ev = (Evento) eventos.remove();
		} catch (Exception except) {
			except.printStackTrace();
		}
		setTempoSimulacao(ev.getTempoEvento());

		//Certifica que não vai terminar na fase transiente
		if (getRodadas().size() == 1) {
			if (getRodadaAtual().getQtdClientesRodada() == NUMERO_AMOSTRAS_FASE_TRANSIENTE)
				tempoUltimosIncrementosFila2 = 0;
			if (getRodadaAtual().getQtdClientesRodada() > NUMERO_AMOSTRAS_FASE_TRANSIENTE) {
				if (((getRodadaAtual().getQtdClientesRodada() - NUMERO_AMOSTRAS_FASE_TRANSIENTE) % INCREMENTO_DA_FASE_TRANSIENTE) == 0) {
					getRodadaAtual().finalizaRodada();
					double esperancaT2DosUltimos = tempoUltimosIncrementosFila2
							/ INCREMENTO_DA_FASE_TRANSIENTE;
					if (Math.abs(esperancaT2DosUltimos
							- getRodadaAtual().getMediaT2()) <= CONDICAO_DE_EQUILIBRIO) {
						this.result += "Numero fregueses da fase transiente: "
								+ String.valueOf(getRodadaAtual()
										.getQtdClientesRodada());
						setRodadaAtual(new RodadaSim(
								getRodadas().size() + 1));
					}
					tempoUltimosIncrementosFila2 = 0;
				}
			}
		//Termina a rodada
		} else if (NUMERO_AMOSTRAS_RODADA == getRodadaAtual()
				.getQtdClientesRodada()) {
			getRodadaAtual().finalizaRodada();
			if (calculaEstatisticasSimulacao()) {
				return false;
			}
			;
			setRodadaAtual(new RodadaSim(getRodadas().size() + 1));
		}

		//Trata os eventos de chegada de cliente
		if (ev.getTipoEventoEnum() == Evento.EnumEventos.CHEGADA_CLIENTE) {
			//Adiciona o cliente na fila 1
			geraEventoChegadaFregues();
			getFila1Sistema().adicionarCliente(ev.getCliente());
			//Verifica se o servidor está vazio
			if (getServidorSistema().estaVazio()) {
				//Servidor Vazio => Atende o cliente
				getServidorSistema().atendeCliente(
						getFila1Sistema().removerCliente());
				//Gero o evento de fim de serviço pra ele
				geraEventoFimServico();
			} else {
				//Servidor ocupado
				//Verifico se o cliente é do sistema 1 ou 2
				if (getServidorSistema().getClienteSendoAtendido()
						.getQtdServicos() >= 1) {
					//Servidor ocupado por cliente do sistema 2.
					//Tirar o cliente em serviço do servidor
					//Alterar o evento de fim de serviço desse cliente
					//Devolver esse cliente pro inicio da fila 2
					alteraEventoFimServico(getServidorSistema()
							.getClienteSendoAtendido().getEventoFimServico());
					getFila2Sistema().devolverCliente(
							getServidorSistema().getClienteSendoAtendido());
					
					//Atender o cliente que chegou na fila 1
					getServidorSistema().atendeCliente(
							getFila1Sistema().removerCliente());
					geraEventoFimServico();
				}else
					//Servidor ocupado por cliente do sistema 1.
					//Não faço nada, deixo ele na fila 1.
				{;}

			}

		//Trata os eventos de fim de serviço
		} else if (ev.getTipoEventoEnum() == Evento.EnumEventos.FIM_SERVICO) {
			//Verifico se é o serviço da fila 1
			if (ev.getCliente().getQtdServicos() == 0) {
				//Término de serviço da fila 1.
				//Removo cliente do serviço e mando pra fila 2
				getServidorSistema().removeClienteAtendimento();
				getFila2Sistema().adicionarCliente(ev.getCliente());
			} else {
				//Término de serviço da fila 2.
				//Removo cliente do atendimento e do sistema
				getServidorSistema().removeClienteAtendimento();
			}
			
			//Servidor ocioso.
			//Verifico se a fila 1 não está vazia
			if (!getFila1Sistema().estaVazia()) {
				//Fila 1 não está vazia. Removo cliente da fila para atender.
				getServidorSistema().atendeCliente(
						getFila1Sistema().removerCliente());
				geraEventoFimServico();
			//Verifico se a fila 2 não está vazia
			} else if (!getFila2Sistema().estaVazia()) {
				//Fila 2 não está vazia. Removo cliente da fila para atender.
				getServidorSistema().atendeCliente(
						getFila2Sistema().removerCliente());
				geraEventoFimServico();
			}
		}
		return true;
	}

	private void geraEventoChegadaFregues() {
		double tempoEventoSeguinte = getTempoSimulacao() + taxaChegada.fdraw();
		Cliente clienteNovo = new Cliente();
		clienteNovo.setNumeroRodadaDoServico(rodadas.size());

		//Jogo um evento novo na fila de eventos
		eventos.add(new Evento(Evento.EnumEventos.CHEGADA_CLIENTE,
				tempoEventoSeguinte, clienteNovo));

		//Populo o cliente do evento
		setIdEntradaFreguesSistema(getIdEntradaFreguesSistema() + 1);
		clienteNovo.setIdCliente(getIdEntradaFreguesSistema());

		//Popula variaveis para a estatistica
		getRodadaAtual().setSomaNq1(
				getRodadaAtual().getSomaNq1()
						+ getFila1Sistema().getQtdClientesFila());
		getRodadaAtual().setSomaNq2(
				getRodadaAtual().getSomaNq2()
						+ getFila2Sistema().getQtdClientesFila());
		if (getServidorSistema().estaVazio()) {
			getRodadaAtual().setSomaN1(
					getRodadaAtual().getSomaN1()
							+ getFila1Sistema().getQtdClientesFila());
			getRodadaAtual().setSomaN2(
					getRodadaAtual().getSomaN2()
							+ getFila2Sistema().getQtdClientesFila());
		} else if (getServidorSistema().getClienteSendoAtendido()
				.getQtdServicos() == 0) {
			getRodadaAtual().setSomaN1(
					getRodadaAtual().getSomaN1()
							+ getFila1Sistema().getQtdClientesFila() + 1);
			getRodadaAtual().setSomaN2(
					getRodadaAtual().getSomaN2()
							+ getFila2Sistema().getQtdClientesFila());
		} else if (getServidorSistema().getClienteSendoAtendido()
				.getQtdServicos() >= 1) {
			getRodadaAtual().setSomaN1(
					getRodadaAtual().getSomaN1()
							+ getFila1Sistema().getQtdClientesFila());
			getRodadaAtual().setSomaN2(
					getRodadaAtual().getSomaN2()
							+ getFila2Sistema().getQtdClientesFila() + 1);
		}

	}

	private void geraEventoFimServico() {
		//Chamo o gerador para gerar um tempo de serviço
		double tempoServicoGerado = taxaSaida.fdraw();
		double tempoServicoRestante;
		Cliente cliente = new Cliente();
		Evento evento;
		cliente = getServidorSistema().getClienteSendoAtendido();
		tempoServicoRestante = cliente.getTempoRestanteServico();

		//Avalia se ja foi interrompido ou nao. Se ja, o tempo de servico sera o tempo de servico restante.
		if (cliente.getFoiInterrompido()) {
			evento = new Evento(Evento.EnumEventos.FIM_SERVICO,
					getTempoSimulacao() + tempoServicoRestante, cliente);
			eventos.add(evento);
			cliente.setEventoFimServico(evento);
		//Se nao, sera um tempo de servico gerado com taxa txServico
		} else {
			evento = new Evento(Evento.EnumEventos.FIM_SERVICO,
					getTempoSimulacao() + tempoServicoGerado, cliente);
			eventos.add(evento);
			cliente.setEventoFimServico(evento);
			cliente.setTempoRestanteServico(tempoServicoGerado);
			cliente.setTempoTotalServico(tempoServicoGerado);
		}

	}

	private void alteraEventoFimServico(Evento eventoFimServico) {
		Cliente cliente;
		cliente = eventoFimServico.getCliente();

		// Remove o primeiro evento de fim de servico da lista de eventos
		eventos.remove(eventoFimServico);

		// Passo o tempo de servico restante pra variavel do cliente desse servico
		if (!cliente.getFoiInterrompido())
			cliente.setTempoRestanteServico(cliente.getTempoTotalServico()
					- (getTempoSimulacao() - cliente.get2VezNoServidor()));
		else
			cliente.setTempoRestanteServico(cliente.getTempoRestanteServico()
					- (getTempoSimulacao() - cliente.getNVezServidor()));

		// Seto que ele ja foi interrompido
		cliente.setFoiInterrompido(true);
	}

	public void setLambdaDistribuicaoChegada(double taxaChegada) {
		this.taxaChegada.taxa = taxaChegada;
	}

	public void setLambdaDistribuicaoServico(double taxaServico) {
		this.taxaSaida.taxa = taxaServico;
	}

	private boolean calculaEstatisticasSimulacao() {

		double valorMedioN1 = 0.0, valorMedioN2 = 0.0;
		double valorMedioT1 = 0.0, valorMedioT2 = 0.0;
		double valorMedioNq1 = 0.0, valorMedioNq2 = 0.0;
		double valorMedioW1 = 0.0, valorMedioW2 = 0.0;
		double valorMedioVarianciaW1 = 0.0, valorMedioVarianciaW2 = 0.0;

		double valorVarianciaN1 = 0.0, varianciaAmostralN2 = 0.0;
		double valorVarianciaT1 = 0.0, valorVarianciaT2 = 0.0;
		double valorVarianciaNq1 = 0.0, varianciaAmostralNq2 = 0.0;
		double valorVarianciaW1 = 0.0, valorVarianciaW2 = 0.0;
		double valorVarianciaDalVarianciaW1 = 0.0, valorVarianciaDalVarianciaW2 = 0.0;

		double valorRaioICN1 = 0.0, valorRaioICN2 = 0.0;
		double valorRaioICNq1 = 0.0, valorRaioICNq2 = 0.0;
		double valorRaioICT1 = 0.0, valorRaioICT2 = 0.0;
		double valorRaioICW1 = 0.0, valorRaioICW2 = 0.0;
		double valorRaioICVarianciaW1 = 0.0, valorRaioICVarianciaW2 = 0.0;

		for (int i = 1; i < getRodadas().size(); i++) {
			valorMedioN1 += ((RodadaSim) getRodadas().get(i))
					.getMediaN1() / (getRodadas().size() - 1);
			valorMedioN2 += ((RodadaSim) getRodadas().get(i))
					.getMediaN2() / (getRodadas().size() - 1);
			valorMedioT1 += ((RodadaSim) getRodadas().get(i))
					.getMediaT1() / (getRodadas().size() - 1);
			valorMedioT2 += ((RodadaSim) getRodadas().get(i))
					.getMediaT2() / (getRodadas().size() - 1);
			valorMedioNq1 += ((RodadaSim) getRodadas().get(i))
					.getMediaNq1() / (getRodadas().size() - 1);
			valorMedioNq2 += ((RodadaSim) getRodadas().get(i))
					.getMediaNq2() / (getRodadas().size() - 1);
			valorMedioW1 += ((RodadaSim) getRodadas().get(i))
					.getMediaW1() / (getRodadas().size() - 1);
			valorMedioW2 += ((RodadaSim) getRodadas().get(i))
					.getMediaW2() / (getRodadas().size() - 1);
			valorMedioVarianciaW1 += ((RodadaSim) getRodadas().get(i))
					.getVarianciaW1() / (getRodadas().size() - 1);
			valorMedioVarianciaW2 += ((RodadaSim) getRodadas().get(i))
					.getVarianciaW2() / (getRodadas().size() - 1);
		}

		for (int i = 1; i < getRodadas().size(); i++) {
			valorVarianciaN1 += ((((RodadaSim) getRodadas().get(i))
					.getMediaN1() - valorMedioN1) * (((RodadaSim) getRodadas()
					.get(i)).getMediaN1() - valorMedioN1))
					/ (getRodadas().size() - 1);
			varianciaAmostralN2 += ((((RodadaSim) getRodadas().get(i))
					.getMediaN2() - valorMedioN2) * (((RodadaSim) getRodadas()
					.get(i)).getMediaN2() - valorMedioN2))
					/ (getRodadas().size() - 1);
			valorVarianciaT1 += ((((RodadaSim) getRodadas().get(i))
					.getMediaT1() - valorMedioT1) * (((RodadaSim) getRodadas()
					.get(i)).getMediaT1() - valorMedioT1))
					/ (getRodadas().size() - 1);
			valorVarianciaT2 += ((((RodadaSim) getRodadas().get(i))
					.getMediaT2() - valorMedioT2) * (((RodadaSim) getRodadas()
					.get(i)).getMediaT2() - valorMedioT2))
					/ (getRodadas().size() - 1);
			valorVarianciaNq1 += ((((RodadaSim) getRodadas().get(i))
					.getMediaNq1() - valorMedioNq1) * (((RodadaSim) getRodadas()
					.get(i)).getMediaNq1() - valorMedioNq1))
					/ (getRodadas().size() - 1);
			varianciaAmostralNq2 += ((((RodadaSim) getRodadas().get(i))
					.getMediaNq2() - valorMedioNq2) * (((RodadaSim) getRodadas()
					.get(i)).getMediaNq2() - valorMedioNq2))
					/ (getRodadas().size() - 1);
			valorVarianciaW1 += ((((RodadaSim) getRodadas().get(i))
					.getMediaW1() - valorMedioW1) * (((RodadaSim) getRodadas()
					.get(i)).getMediaW1() - valorMedioW1))
					/ (getRodadas().size() - 1);
			valorVarianciaW2 += ((((RodadaSim) getRodadas().get(i))
					.getMediaW2() - valorMedioW2) * (((RodadaSim) getRodadas()
					.get(i)).getMediaW2() - valorMedioW2))
					/ (getRodadas().size() - 1);
			valorVarianciaDalVarianciaW1 += ((((RodadaSim) getRodadas()
					.get(i)).getVarianciaW1() - valorMedioVarianciaW1) * (((RodadaSim) getRodadas()
					.get(i)).getVarianciaW1() - valorMedioVarianciaW1))
					/ (getRodadas().size() - 1);
			valorVarianciaDalVarianciaW2 += ((((RodadaSim) getRodadas()
					.get(i)).getVarianciaW2() - valorMedioVarianciaW2) * (((RodadaSim) getRodadas()
					.get(i)).getVarianciaW2() - valorMedioVarianciaW2))
					/ (getRodadas().size() - 1);

		}

		if (getRodadas().size() - 1 > 2) {
			double distribuicaoTStudent95[] = { 12.706, 4.303, 3.182, 2.776,
					2.571, 2.447, 2.365, 2.306, 2.262, 2.228, 2.201, 2.179,
					2.160, 2.145, 2.131, 2.120, 2.110, 2.101, 2.093, 2.086,
					2.080, 2.074, 2.069, 2.064, 2.060, 2.056, 2.052, 2.048,
					2.045, 2.042 };

			int grausLiberdade = getRodadas().size() - 1;
			if (grausLiberdade > 30)
				grausLiberdade = 30;
			double distribuicao = distribuicaoTStudent95[grausLiberdade - 1];

			valorRaioICN1 = distribuicao * Math.sqrt(valorVarianciaN1)
					/ Math.sqrt((double) getRodadas().size() - 1);
			valorRaioICN2 = distribuicao * Math.sqrt(varianciaAmostralN2)
					/ Math.sqrt((double) getRodadas().size() - 1);
			valorRaioICNq1 = distribuicao * Math.sqrt(valorVarianciaNq1)
					/ Math.sqrt((double) getRodadas().size() - 1);
			valorRaioICNq2 = distribuicao * Math.sqrt(varianciaAmostralNq2)
					/ Math.sqrt((double) getRodadas().size() - 1);
			valorRaioICT1 = distribuicao * Math.sqrt(valorVarianciaT1)
					/ Math.sqrt((double) getRodadas().size() - 1);
			valorRaioICT2 = distribuicao * Math.sqrt(valorVarianciaT2)
					/ Math.sqrt((double) getRodadas().size() - 1);
			valorRaioICW1 = distribuicao * Math.sqrt(valorVarianciaW1)
					/ Math.sqrt((double) getRodadas().size() - 1);
			valorRaioICW2 = distribuicao * Math.sqrt(valorVarianciaW2)
					/ Math.sqrt((double) getRodadas().size() - 1);
			valorRaioICVarianciaW1 = distribuicao
					* Math.sqrt(valorVarianciaDalVarianciaW1)
					/ Math.sqrt((double) getRodadas().size() - 1);
			valorRaioICVarianciaW2 = distribuicao
					* Math.sqrt(valorVarianciaDalVarianciaW2)
					/ Math.sqrt((double) getRodadas().size() - 1);

			boolean intervalosCondizentes = true;

			if ((2 * valorRaioICN1 / valorMedioN1) >= INTERVALO_DE_CONFIANCA)
				intervalosCondizentes = false;
			else if ((2 * valorRaioICT1 / valorMedioT1) >= INTERVALO_DE_CONFIANCA)
				intervalosCondizentes = false;
			else if ((2 * valorRaioICNq1 / valorMedioNq1) >= INTERVALO_DE_CONFIANCA)
				intervalosCondizentes = false;
			else if ((2 * valorRaioICW1 / valorMedioW1) >= INTERVALO_DE_CONFIANCA)
				intervalosCondizentes = false;
			else if ((2 * valorRaioICN2 / valorMedioN2) >= INTERVALO_DE_CONFIANCA)
				intervalosCondizentes = false;
			else if ((2 * valorRaioICT2 / valorMedioT2) >= INTERVALO_DE_CONFIANCA)
				intervalosCondizentes = false;
			else if ((2 * valorRaioICNq2 / valorMedioNq2) >= INTERVALO_DE_CONFIANCA)
				intervalosCondizentes = false;
			else if ((2 * valorRaioICW2 / valorMedioW2) >= INTERVALO_DE_CONFIANCA)
				intervalosCondizentes = false;
			else if ((2 * valorRaioICVarianciaW1 / valorMedioVarianciaW1) >= INTERVALO_DE_CONFIANCA)
				intervalosCondizentes = false;
			else if ((2 * valorRaioICVarianciaW2 / valorMedioVarianciaW2) >= INTERVALO_DE_CONFIANCA)
				intervalosCondizentes = false;

			if (intervalosCondizentes == false)
				return false;
			else {
				
				this.result += ("\nSimulaÃ§Ã£o Terminada ...");
				
				this.result += ("\n Resultado Final : \n ");
				this.result += (" Valor MÃ©dio Estimado  -  Intervalo de ConfiaÃ§a \n");

				this.result += ("\n E[N1]    " + valorMedioN1 + "  "
						+ (2 * valorRaioICN1 / valorMedioN1) * 100);
				this.result += ("\n E[N2]    " + valorMedioN2 + "  "
						+ (2 * valorRaioICN2 / valorMedioN2) * 100);
				this.result += ("\n E[T1]    " + valorMedioT1 + "  "
						+ (2 * valorRaioICT1 / valorMedioT1) * 100);
				this.result += ("\n  E[T2]    " + valorMedioT2 + "  "
						+ (2 * valorRaioICT2 / valorMedioT2) * 100);
				this.result += ("\n E[Nq1]    " + valorMedioNq1 + "  "
						+ (2 * valorRaioICNq1 / valorMedioNq1) * 100);
				this.result += ("\n E[Nq2]    " + valorMedioNq2 + "  "
						+ (2 * valorRaioICNq2 / valorMedioNq2) * 100);
				this.result += ("\n E[W1]    " + valorMedioW1 + "  "
						+ (2 * valorRaioICW1 / valorMedioW1) * 100);
				this.result += ("\n E[W2]    " + valorMedioW2 + "  "
						+ (2 * valorRaioICW1 / valorMedioW1) * 100);
				this.result += ("\n E[V[W1]]    " + valorMedioVarianciaW1 + "  "
						+ (2 * valorRaioICVarianciaW1 / valorMedioVarianciaW1)
						* 100);
				this.result += ("\n E[V[W2]]    " + valorMedioVarianciaW2 + "  "
						+ (2 * valorRaioICVarianciaW2 / valorMedioVarianciaW2)
						* 100);

				return true;
			}
		} else
			return false;
	}

	public double getTempoSimulacao() {
		return tempoSimulacao;
	}

	public void setTempoSimulacao(double tempo) {
		GeraEventoSim.tempoSimulacao = tempo;
	}

	private Fila getFila1Sistema() {
		return fila1Sistema;
	}

	private void setFila1Sistema(Fila f1) {
		this.fila1Sistema = f1;
	}

	private Fila getFila2Sistema() {
		return fila2Sistema;
	}

	private void setFila2Sistema(Fila f2) {
		this.fila2Sistema = f2;
	}

	private Servidor getServidorSistema() {
		return servidorSistema;
	}

	private void setServidorSistema(Servidor s) {
		this.servidorSistema = s;
	}

	public RodadaSim getRodadaAtual() {
		return (RodadaSim) rodadas.getLast();
	}

	public void setRodadaAtual(RodadaSim rodada) {
		rodadas.addLast(rodada);
	}

	public LinkedList<RodadaSim> getRodadas() {
		return rodadas;
	}

	public void setRodadas(LinkedList<RodadaSim> lista) {
		this.rodadas = lista;
	}

	public int getIdEntradaFreguesSistema() {
		return idEntradaFreguesSistema;
	}

	public void setIdEntradaFreguesSistema(int id) {
		this.idEntradaFreguesSistema = id;
	}

	public void setResult(String result) {
		this.result = result;
	}

	public String getResult() {
		return result;
	}

}
